//go:build windows && windows
// +build windows,windows

package utils

import (
	"fmt"
	"regexp"
	"runtime"
	"syscall"
	"unsafe"
)

// Copyright 2013 @atotto. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

import (
	"time"
)

const (
	cfUnicodetext = 13
	gmemMoveable  = 0x0002
)

var (
	user32_                    = syscall.MustLoadDLL("user32")
	isClipboardFormatAvailable = user32_.MustFindProc("IsClipboardFormatAvailable")
	openClipboard              = user32_.MustFindProc("OpenClipboard")
	closeClipboard             = user32_.MustFindProc("CloseClipboard")
	emptyClipboard             = user32_.MustFindProc("EmptyClipboard")
	getClipboardData           = user32_.MustFindProc("GetClipboardData")
	setClipboardData           = user32_.MustFindProc("SetClipboardData")
	registerClipboardFormat_   = user32_.MustFindProc("RegisterClipboardFormatW")

	kernel32_    = syscall.NewLazyDLL("kernel32")
	globalAlloc  = kernel32_.NewProc("GlobalAlloc")
	globalFree   = kernel32_.NewProc("GlobalFree")
	globalSize   = kernel32_.NewProc("GlobalSize")
	globalLock   = kernel32_.NewProc("GlobalLock")
	globalUnlock = kernel32_.NewProc("GlobalUnlock")
	lstrcpy      = kernel32_.NewProc("lstrcpyW")
)

var srcUrlPattern = regexp.MustCompile("SourceURL:(.*)")

// waitOpenClipboard opens the clipboard, waiting for up to a second to do so.
func waitOpenClipboard() error {
	started := time.Now()
	limit := started.Add(time.Second)
	var r uintptr
	var err error
	for time.Now().Before(limit) {
		r, _, err = openClipboard.Call(0)
		if r != 0 {
			return nil
		}
		time.Sleep(time.Millisecond)
	}
	return err
}

func cfHtml() (result uintptr) {
	ret, _, _ := registerClipboardFormat_.Call(
		uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr("HTML Format"))))
	result = ret
	return
}

func ReadUrlFromCfHtml() (string, error) {
	cfhtml, err := ReadAllFromClipBoard()
	if err != nil {
		return "", err
	}
	submatch := srcUrlPattern.FindAllStringSubmatch(cfhtml, 1)
	if len(submatch) == 1 {
		match := submatch[0][1]
		return match[0 : len(match)-1], nil
	}
	return "", fmt.Errorf("剪切板没有url信息")
}

func ReadAllFromClipBoard() (string, error) {
	// LockOSThread ensure that the whole method will keep executing on the same thread from begin to end (it actually locks the goroutine thread attribution).
	// Otherwise if the goroutine switch thread during execution (which is a common practice), the OpenClipboard and CloseClipboard will happen on two different threads, and it will result in a clipboard deadlock.
	runtime.LockOSThread()
	defer runtime.UnlockOSThread()
	format := cfHtml()
	if formatAvailable, _, _ := isClipboardFormatAvailable.Call(format); formatAvailable == 0 {
		//return "", err
		format = cfUnicodetext
		formatAvailable, _, _ = isClipboardFormatAvailable.Call(cfUnicodetext)
	}
	err := waitOpenClipboard()
	if err != nil {
		return "", err
	}

	h, _, err := getClipboardData.Call(format)
	if h == 0 {
		_, _, _ = closeClipboard.Call()
		return "", err
	}

	size, _, _ := globalSize.Call(h)

	l, _, err := globalLock.Call(h)
	if l == 0 {
		_, _, _ = closeClipboard.Call()
		return "", err
	}
	uint16s := (*[1 << 20]uint16)(unsafe.Pointer(l))[:size]
	uint8s := (*[1 << 20]uint8)(unsafe.Pointer(l))[:size]
	text := ""
	if format == cfUnicodetext {
		text = syscall.UTF16ToString(uint16s)
	} else {
		text = string(uint8s)
	}
	r, _, err := globalUnlock.Call(h)
	if r == 0 {
		_, _, _ = closeClipboard.Call()
		return "", err
	}

	closed, _, err := closeClipboard.Call()
	if closed == 0 {
		return "", err
	}
	return text, nil
}

func WriteAllToClipBoard(text string) error {
	// LockOSThread ensure that the whole method will keep executing on the same thread from begin to end (it actually locks the goroutine thread attribution).
	// Otherwise if the goroutine switch thread during execution (which is a common practice), the OpenClipboard and CloseClipboard will happen on two different threads, and it will result in a clipboard deadlock.
	runtime.LockOSThread()
	defer runtime.UnlockOSThread()

	err := waitOpenClipboard()
	if err != nil {
		return err
	}

	r, _, err := emptyClipboard.Call(0)
	if r == 0 {
		_, _, _ = closeClipboard.Call()
		return err
	}

	data := syscall.StringToUTF16(text)

	// "If the hMem parameter identifies a memory object, the object must have
	// been allocated using the function with the GMEM_MOVEABLE flag."
	h, _, err := globalAlloc.Call(gmemMoveable, uintptr(len(data)*int(unsafe.Sizeof(data[0]))))
	if h == 0 {
		_, _, _ = closeClipboard.Call()
		return err
	}
	defer func() {
		if h != 0 {
			globalFree.Call(h)
		}
	}()

	l, _, err := globalLock.Call(h)
	if l == 0 {
		_, _, _ = closeClipboard.Call()
		return err
	}

	r, _, err = lstrcpy.Call(l, uintptr(unsafe.Pointer(&data[0])))
	if r == 0 {
		_, _, _ = closeClipboard.Call()
		return err
	}

	r, _, err = globalUnlock.Call(h)
	if r == 0 {
		if err.(syscall.Errno) != 0 {
			_, _, _ = closeClipboard.Call()
			return err
		}
	}

	r, _, err = setClipboardData.Call(cfUnicodetext, h)
	if r == 0 {
		_, _, _ = closeClipboard.Call()
		return err
	}
	h = 0 // suppress deferred cleanup
	closed, _, err := closeClipboard.Call()
	if closed == 0 {
		return err
	}
	return nil
}
